
#include "RtBase.h"
#include "RtHttpAuthenticator.h"
#include "RtHttpUrl.h"
#include "RtChannelHttpClient.h"

#ifdef RT_WIN32
#include "RtHttpNTLMAuth.h"
#endif // RT_WIN32

//#ifndef RT_MYMD5
//#include <openssl/md5.h>
//#else
#include "RtMD5.h"
//#endif


//////////////////////////////////////////////////////////////////////
// Class IRtHttpAuthenticator
//////////////////////////////////////////////////////////////////////

IRtHttpAuthenticator::~IRtHttpAuthenticator()
{
}

IRtHttpAuthenticator* IRtHttpAuthenticator::
GetAuthenticatorFromScheme(const CRtString &aScheme)
{
	IRtHttpAuthenticator *retAuth = NULL;

	if (!strcasecmp(aScheme.c_str(), CRtHttpBasicAuth::s_pszBasicAuth)) {
		RT_INFO_TRACE("IRtHttpAuthenticator::GetAuthenticatorFromScheme, Basic");
		retAuth = new CRtHttpBasicAuth();
	} else if (!strcasecmp(aScheme.c_str(), CRtHttpDigestAuth::s_pszDigestAuth)) {
		RT_INFO_TRACE("IRtHttpAuthenticator::GetAuthenticatorFromScheme, Digest");
		retAuth = new CRtHttpDigestAuth();
	}
#ifdef RT_WIN32
	else if (!strcasecmp(aScheme.c_str(), CRtHttpNTLMAuth::s_pszNTLMAuth)) {
		RT_INFO_TRACE("IRtHttpAuthenticator::GetAuthenticatorFromScheme, NTLM");
		retAuth = new CRtHttpNTLMAuth();
	}
#endif // RT_WIN32
	return retAuth;
}

CRtString IRtHttpAuthenticator::
GetSchemeFromChallenge(const CRtString &aChallenge)
{
	CRtString strScheme;
	CRtString::size_type nFind= aChallenge.find(' ');
	if (nFind != CRtString::npos)
		strScheme = aChallenge.substr(0, nFind);
	else
		strScheme = aChallenge;
	return strScheme;
}


//////////////////////////////////////////////////////////////////////
// Class CRtHttpAuthSteps
//////////////////////////////////////////////////////////////////////

CRtHttpAuthSteps::CRtHttpAuthSteps(DWORD aSteps)
	: m_dwAuthSteps(aSteps)
	, m_dwAuthStepsMax(aSteps)
{
}

CRtHttpAuthSteps::~CRtHttpAuthSteps()
{
}

BOOL CRtHttpAuthSteps::HaveAuthenticateComplete()
{
	return (m_dwAuthSteps ? FALSE : TRUE);
}

DWORD CRtHttpAuthSteps::GetAuthStep()
{
	return m_dwAuthStepsMax - m_dwAuthSteps;
}

CRtString CRtHttpAuthSteps::
GenerateCredentials(IRtChannelHttpClient *aChannel, LPCSTR aChallenge, 
					LPCSTR aUsername, LPCSTR aPassword)
{
	CRtString strRet;
	RT_ASSERTE_RETURN(!HaveAuthenticateComplete(), strRet);

	strRet = do_GenerateCredentials(aChannel, aChallenge, aUsername, aPassword);

	m_dwAuthSteps--;
	return strRet;
}


//////////////////////////////////////////////////////////////////////
// Class CRtHttpBasicAuth
//////////////////////////////////////////////////////////////////////

const char * CRtHttpBasicAuth::s_pszBasicAuth = "Basic";

CRtHttpBasicAuth::CRtHttpBasicAuth()
	: CRtHttpAuthSteps(1)
{
}

CRtHttpBasicAuth::~CRtHttpBasicAuth()
{
}

CRtString CRtHttpBasicAuth::
do_GenerateCredentials(IRtChannelHttpClient *aChannel, LPCSTR aChallenge, 
					LPCSTR aUsername, LPCSTR aPassword)
{
	CRtString strRetval;
	RT_ASSERTE_RETURN(aChannel && aChallenge, strRetval);
	RT_INFO_TRACE_THIS("CRtHttpBasicAuth::do_GenerateCredentials,"
		" Username=" << aUsername/* << 
		" Password=" << aPassword*/);
	
	if (!aUsername || !aUsername[0])
		return strRetval;
	if (strncasecmp(aChallenge, s_pszBasicAuth, strlen(s_pszBasicAuth)))
		return strRetval;

	CRtString strUserpass;
	strUserpass += aUsername;
	strUserpass += ':';
	if (aPassword)
		strUserpass += aPassword;

	CRtString sbEncoded;
	RtBase64Encode(
		reinterpret_cast<const BYTE*>(strUserpass.c_str()), 
		strUserpass.length(), 
		sbEncoded);

	strRetval = s_pszBasicAuth;
	strRetval += ' ';
	strRetval += sbEncoded;
	return strRetval;
}

BOOL CRtHttpBasicAuth::AreCredentialsReusable()
{
	return TRUE;
}

BOOL CRtHttpBasicAuth::AllowFirstTryWithoutUserName()
{
	return FALSE;
}

LPCSTR CRtHttpBasicAuth::GetAuthenticateScheme()
{
	return s_pszBasicAuth;
}


//////////////////////////////////////////////////////////////////////
// Class CRtHttpDigestAuth
//////////////////////////////////////////////////////////////////////

const char * CRtHttpDigestAuth::s_pszDigestAuth = "Digest";

#define ALGO_SPECIFIED 0x01
#define ALGO_MD5 0x02
#define ALGO_MD5_SESS 0x04
#define QOP_AUTH 0x01
#define QOP_AUTH_INT 0x02

#define DIGEST_LENGTH 16
#define EXPANDED_DIGEST_LENGTH 32
#define NONCE_COUNT_LENGTH 8

#define HASHLEN 16
typedef unsigned char HASH[HASHLEN];
#define HASHHEXLEN 32
typedef char HASHHEX[HASHHEXLEN+1];

static void DigestCalcHA1(const char * pszAlg, const char * pszUserName, const char * pszRealm, 
				   const char * pszPassword, const char * pszNonce, const char * pszCNonce, 
				   HASHHEX SessionKey);

/* calculate request-digest/response-digest as per HTTP Digest spec */
static void DigestCalcResponse(HASHHEX HA1, /* H(A1) */
						const char * pszNonce, /* nonce from server */
						const char * pszNonceCount, /* 8 hex digits */
						const char * pszCNonce, /* client nonce */
						const char * pszQop, /* qop-value: "", "auth", "auth-int" */
						const char * pszMethod, /* method from the request */
						const char * pszDigestUri, /* requested URL */
						HASHHEX HEntity, /* H(entity body) if qop="auth-int" */
						HASHHEX Response /* request-digest or response-digest */
						);

CRtHttpDigestAuth::CRtHttpDigestAuth()
	: CRtHttpAuthSteps(1)
	, m_dwExtra(0)
{
}

CRtHttpDigestAuth::~CRtHttpDigestAuth()
{
}

CRtString CRtHttpDigestAuth::
do_GenerateCredentials(IRtChannelHttpClient *aChannel, LPCSTR aChallenge, 
					   LPCSTR aUsername, LPCSTR aPassword)
{
	CRtString strRetval;
	RT_ASSERTE_RETURN(aChannel && aChallenge, strRetval);
	RT_INFO_TRACE_THIS("CRtHttpDigestAuth::do_GenerateCredentials,"
		" Username=" << aUsername/* << 
		" Password=" << aPassword*/);

	if (!aUsername || !aUsername[0])
		return strRetval;
	if (strncasecmp(aChallenge, s_pszDigestAuth, strlen(s_pszDigestAuth)))
		return strRetval;

	CRtAutoPtr<CRtHttpUrl> pURL;
	RtResult rv = aChannel->GetUrl(pURL.ParaOut());
	if (RT_FAILED(rv))
		return strRetval;

	CRtString strPath = pURL->GetPath();
	CRtString strMethod;
	rv = aChannel->GetRequestMethod(strMethod);
	if (RT_FAILED(rv))
		return strRetval;

	CRtString strRealm, strNonce, strOpaque;
	WORD nAlgorithm, nQop;
	if (!ParseChallenge(aChallenge, strRealm, strNonce, strOpaque, nAlgorithm, nQop)) {
		RT_ERROR_TRACE_THIS("CRtHttpDigestAuth::::do_GenerateCredentials,"
			" ParseChallenge() failed!");
		return strRetval;
	}

	if (nQop & QOP_AUTH_INT) {
		// we do not support auth-int "quality of protection" currently
		nQop &= ~QOP_AUTH_INT;
	}
	if (!(nAlgorithm & ALGO_MD5 || nAlgorithm & ALGO_MD5_SESS)) {
		// they asked only for algorithms that we do not support
		RT_ERROR_TRACE_THIS("CRtHttpDigestAuth::::do_GenerateCredentials,"
			" unsupported algorithm requested by Digest authentication");
		return strRetval;
	}
	
	char szNonceCount[NONCE_COUNT_LENGTH+1]; // in hex
	snprintf(szNonceCount, sizeof(szNonceCount), "%08x", ++m_dwExtra);

	char szCnonce[16]={0};
	static const char hexChar[] = "0123456789abcdef"; 
	for (size_t i = 0; i < sizeof(szCnonce); ++i) {
		szCnonce[i] = (hexChar[(int)(15.0 * rand()/(RAND_MAX + 1.0))]);
	}

	HASHHEX HA1;
	HASHHEX HA2 = "";
	HASHHEX szResponse;
	DigestCalcHA1(nAlgorithm & ALGO_MD5 ? "MD5" : "MD5-sess", aUsername, 
		strRealm.c_str(), aPassword, strNonce.c_str(), szCnonce, HA1);

	DigestCalcResponse(HA1, strNonce.c_str(), szNonceCount, szCnonce, 
		nQop ? (nQop & QOP_AUTH ? "auth" : "auth-int") : "", 
		strMethod.c_str(), strPath.c_str(), HA2, szResponse);

	strRetval.reserve(256);
	strRetval += s_pszDigestAuth;
	strRetval += " username=\"";
	strRetval += aUsername;
	strRetval += "\", realm=\"";
	strRetval += strRealm;
	strRetval += "\", nonce=\"";
	strRetval += strNonce;
	strRetval += "\", uri=\"";
	strRetval += strPath;
	if (nAlgorithm & ALGO_SPECIFIED) {
		strRetval += "\", algorithm=";
		if (nAlgorithm & ALGO_MD5_SESS)
			strRetval += "MD5-sess";
		else
			strRetval += "MD5";
	} else {
		strRetval += "\"";
	}
	strRetval += ", response=\"";
	strRetval += szResponse;
	
	if (!strOpaque.empty()) {
		strRetval += "\", opaque=\"";
		strRetval += strOpaque;
	}
	
	if (nQop) {
		strRetval += "\", qop=";
		if (nQop & QOP_AUTH_INT)
			strRetval += "auth-int";
		else
			strRetval += "auth";
		strRetval += ", nc=";
		strRetval += szNonceCount;
		strRetval += ", cnonce=\"";
		strRetval += szCnonce;
	}
	strRetval += "\"";

	return strRetval;
}

BOOL CRtHttpDigestAuth::AreCredentialsReusable()
{
	return FALSE;
}

BOOL CRtHttpDigestAuth::AllowFirstTryWithoutUserName()
{
	return FALSE;
}

LPCSTR CRtHttpDigestAuth::GetAuthenticateScheme()
{
	return s_pszDigestAuth;
}

BOOL CRtHttpDigestAuth::
ParseChallenge(const char *aChallenge, CRtString &strRealm, 
			   CRtString &strNonce, CRtString &strOpaque, 
			   WORD &nAlgorithm, WORD &nQop)
{
	const char *p = aChallenge + 7; // first 7 characters are "Digest "

	nAlgorithm = ALGO_MD5; // default is MD5
	nQop = 0;
	
	for (;;) {
		while (*p && (*p == ',' || isspace(*p)))
			++p;
		if (!*p)
			break;
		
		// name
		short nameStart = (p - aChallenge);
		while (*p && !isspace(*p) && *p != '=') 
			++p;
		if (!*p)
			return FALSE;
		short nameLength = (p - aChallenge) - nameStart;
		
		while (*p && (isspace(*p) || *p == '=')) 
			++p;
		if (!*p) 
			return FALSE;
		
		BOOL quoted = FALSE;
		if (*p == '"') {
			++p;
			quoted = TRUE;
		}
		
		// value
		short valueStart = (p - aChallenge);
		short valueLength = 0;
		if (quoted) {
			while (*p && *p != '"') 
				++p;
			if (*p != '"') 
				return FALSE;
			valueLength = (p - aChallenge) - valueStart;
			++p;
		} else {
			while (*p && !isspace(*p) && *p != ',') 
				++p; 
			valueLength = (p - aChallenge) - valueStart;
		}
		
		// extract information
		if (nameLength == 5 &&
			strncasecmp(aChallenge+nameStart, "realm", 5) == 0)
		{
			strRealm.append(aChallenge+valueStart, valueLength);
		}
		else if (nameLength == 6 &&
			strncasecmp(aChallenge+nameStart, "domain", 6) == 0)
		{
			//strDomain.append(aChallenge+valueStart, valueLength);
		}
		else if (nameLength == 5 &&
			strncasecmp(aChallenge+nameStart, "nonce", 5) == 0)
		{
			strNonce.append(aChallenge+valueStart, valueLength);
		}
		else if (nameLength == 6 &&
			strncasecmp(aChallenge+nameStart, "opaque", 6) == 0)
		{
			strOpaque.append(aChallenge+valueStart, valueLength);
		}
		else if (nameLength == 5 &&
			strncasecmp(aChallenge+nameStart, "stale", 5) == 0)
		{
			WORD stale;
			if (strncasecmp(aChallenge+valueStart, "true", 4) == 0)
				stale = 1;
			else
				stale = 0;
		}
		else if (nameLength == 9 &&
			strncasecmp(aChallenge+nameStart, "algorithm", 9) == 0)
		{
			// we want to clear the default, so we use = not |= here
			nAlgorithm = ALGO_SPECIFIED;
			if (valueLength == 3 &&
				strncasecmp(aChallenge+valueStart, "MD5", 3) == 0)
				nAlgorithm |= ALGO_MD5;
			else if (valueLength == 8 &&
				strncasecmp(aChallenge+valueStart, "MD5-sess", 8) == 0)
				nAlgorithm |= ALGO_MD5_SESS;
		}
		else if (nameLength == 3 &&
			strncasecmp(aChallenge+nameStart, "qop", 3) == 0)
		{
			short ipos = valueStart;
			while (ipos < valueStart+valueLength) {
				while (ipos < valueStart+valueLength &&
					(isspace(aChallenge[ipos]) ||
					aChallenge[ipos] == ',')) 
					ipos++;
				short algostart = ipos;
				while (ipos < valueStart+valueLength &&
					!isspace(aChallenge[ipos]) &&
					aChallenge[ipos] != ',') 
					ipos++;
				if ((ipos - algostart) == 4 &&
					strncasecmp(aChallenge+algostart, "auth", 4) == 0)
					nQop |= QOP_AUTH;
				else if ((ipos - algostart) == 8 &&
					strncasecmp(aChallenge+algostart, "auth-int", 8) == 0)
					nQop |= QOP_AUTH_INT;
			}
		}
	}
	return TRUE;
}


//
// following encode arithmetic is copyed from the example of rfc2617
//

void CvtHex(HASH Bin, HASHHEX Hex)
{
	unsigned short i;
	unsigned char j;
	for (i = 0; i < HASHLEN; i++) {
		j = (Bin[i] >> 4) & 0xf;
		if (j <= 9)
			Hex[i*2] = (j + '0');
		else
			Hex[i*2] = (j + 'a' - 10);
		j = Bin[i] & 0xf;
		if (j <= 9)
			Hex[i*2+1] = (j + '0');
		else
			Hex[i*2+1] = (j + 'a' - 10);
	};
	Hex[HASHHEXLEN] = '\0';
};

/* calculate H(A1) as per spec */
void DigestCalcHA1(const char * pszAlg, const char * pszUserName, const char * pszRealm, 
				   const char * pszPassword, const char * pszNonce, const char * pszCNonce, 
				   HASHHEX SessionKey)
{
	MD5_CTX Md5Ctx;
	HASH HA1;

#ifdef RT_MYMD5
	RtMD5Init(&Md5Ctx);
	RtMD5Update(&Md5Ctx, (const unsigned char*)pszUserName, strlen(pszUserName));
	RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
	RtMD5Update(&Md5Ctx, (const unsigned char*)pszRealm, strlen(pszRealm));
	RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
	RtMD5Update(&Md5Ctx, (const unsigned char*)pszPassword, strlen(pszPassword));
	//RtMD5Final(HA1, &Md5Ctx);
	RtMD5Final(&Md5Ctx);
	memcpy(HA1, Md5Ctx.digest,16);
	if (strcasecmp(pszAlg, "md5-sess") == 0) {
		RtMD5Init(&Md5Ctx);
		RtMD5Update(&Md5Ctx, (const unsigned char*)HA1, HASHLEN);
		RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
		RtMD5Update(&Md5Ctx, (const unsigned char*)pszNonce, strlen(pszNonce));
		RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
		RtMD5Update(&Md5Ctx, (const unsigned char*)pszCNonce, strlen(pszCNonce));
		//RtMD5Final(HA1, &Md5Ctx);
		RtMD5Final(&Md5Ctx);
		memcpy(HA1, Md5Ctx.digest,16);
	};

#else
	MD5_Init(&Md5Ctx);
	MD5_Update(&Md5Ctx, pszUserName, strlen(pszUserName));
	MD5_Update(&Md5Ctx, ":", 1);
	MD5_Update(&Md5Ctx, pszRealm, strlen(pszRealm));
	MD5_Update(&Md5Ctx, ":", 1);
	MD5_Update(&Md5Ctx, pszPassword, strlen(pszPassword));
	MD5_Final(HA1, &Md5Ctx);
	if (strcasecmp(pszAlg, "md5-sess") == 0) {
		MD5_Init(&Md5Ctx);
		MD5_Update(&Md5Ctx, HA1, HASHLEN);
		MD5_Update(&Md5Ctx, ":", 1);
		MD5_Update(&Md5Ctx, pszNonce, strlen(pszNonce));
		MD5_Update(&Md5Ctx, ":", 1);
		MD5_Update(&Md5Ctx, pszCNonce, strlen(pszCNonce));
		MD5_Final(HA1, &Md5Ctx);
	};
#endif
	CvtHex(HA1, SessionKey);
};

/* calculate request-digest/response-digest as per HTTP Digest spec */
void DigestCalcResponse(HASHHEX HA1, /* H(A1) */
						const char * pszNonce, /* nonce from server */
						const char * pszNonceCount, /* 8 hex digits */
						const char * pszCNonce, /* client nonce */
						const char * pszQop, /* qop-value: "", "auth", "auth-int" */
						const char * pszMethod, /* method from the request */
						const char * pszDigestUri, /* requested URL */
						HASHHEX HEntity, /* H(entity body) if qop="auth-int" */
						HASHHEX Response /* request-digest or response-digest */
						)
{
	MD5_CTX Md5Ctx;
	HASH HA2;
	HASH RespHash;
	HASHHEX HA2Hex;
	// calculate H(A2)
#ifdef RT_MYMD5
	RtMD5Init(&Md5Ctx);
	RtMD5Update(&Md5Ctx, (const unsigned char*)pszMethod, strlen(pszMethod));
	RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
	RtMD5Update(&Md5Ctx, (const unsigned char*)pszDigestUri, strlen(pszDigestUri));
	if (strcasecmp(pszQop, "auth-int") == 0) {
		RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
		RtMD5Update(&Md5Ctx, (const unsigned char*)HEntity, HASHHEXLEN);
	};
	//RtMD5Final(HA2, &Md5Ctx);
	RtMD5Final(&Md5Ctx);
	memcpy(HA2, Md5Ctx.digest, 16);
	CvtHex(HA2, HA2Hex);
	// calculate response
	RtMD5Init(&Md5Ctx);
	RtMD5Update(&Md5Ctx, (const unsigned char*)HA1, HASHHEXLEN);
	RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
	RtMD5Update(&Md5Ctx, (const unsigned char*)pszNonce, strlen(pszNonce));
	RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
	if (*pszQop) {
		RtMD5Update(&Md5Ctx, (const unsigned char*)pszNonceCount, strlen(pszNonceCount));
		RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
		RtMD5Update(&Md5Ctx, (const unsigned char*)pszCNonce, strlen(pszCNonce));
		RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
		RtMD5Update(&Md5Ctx, (const unsigned char*)pszQop, strlen(pszQop));
		RtMD5Update(&Md5Ctx, (const unsigned char*)":", 1);
	};
	RtMD5Update(&Md5Ctx, (const unsigned char*)HA2Hex, HASHHEXLEN);
	//RtMD5Final(RespHash, &Md5Ctx);
	RtMD5Final(&Md5Ctx);
	memcpy(RespHash, Md5Ctx.digest, 16);

#else
	MD5_Init(&Md5Ctx);
	MD5_Update(&Md5Ctx, pszMethod, strlen(pszMethod));
	MD5_Update(&Md5Ctx, ":", 1);
	MD5_Update(&Md5Ctx, pszDigestUri, strlen(pszDigestUri));
	if (strcasecmp(pszQop, "auth-int") == 0) {
		MD5_Update(&Md5Ctx, ":", 1);
		MD5_Update(&Md5Ctx, HEntity, HASHHEXLEN);
	};
	MD5_Final(HA2, &Md5Ctx);
	CvtHex(HA2, HA2Hex);
	// calculate response
	MD5_Init(&Md5Ctx);
	MD5_Update(&Md5Ctx, HA1, HASHHEXLEN);
	MD5_Update(&Md5Ctx, ":", 1);
	MD5_Update(&Md5Ctx, pszNonce, strlen(pszNonce));
	MD5_Update(&Md5Ctx, ":", 1);
	if (*pszQop) {
		MD5_Update(&Md5Ctx, pszNonceCount, strlen(pszNonceCount));
		MD5_Update(&Md5Ctx, ":", 1);
		MD5_Update(&Md5Ctx, pszCNonce, strlen(pszCNonce));
		MD5_Update(&Md5Ctx, ":", 1);
		MD5_Update(&Md5Ctx, pszQop, strlen(pszQop));
		MD5_Update(&Md5Ctx, ":", 1);
	};
	MD5_Update(&Md5Ctx, HA2Hex, HASHHEXLEN);
	MD5_Final(RespHash, &Md5Ctx);
#endif
	CvtHex(RespHash, Response);
};
